1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.util..string; 12 public import hip.util.conv:to; 13 public import hip.util.to_string_range; 14 import core.stdc.string; 15 16 version(WebAssembly) version = UseDRuntimeDecoder; 17 version(CustomRuntimeTest) version = UseDRuntimeDecoder; 18 version(PSVita) version = UseDRuntimeDecoder; 19 version(WebAssembly) version = AvoidStringFragmentation; 20 21 /** 22 * RefCounted, @nogc string, OutputRange compatible, 23 */ 24 struct String 25 { 26 @nogc: 27 import core.stdc.stdlib; 28 import core.int128; 29 char[] chars; 30 private size_t _capacity; 31 private int* countPtr; 32 size_t length() @safe pure const nothrow {return chars.length;} 33 34 this(this) nothrow @safe pure 35 { 36 if(countPtr !is null) 37 *countPtr = *countPtr + 1; 38 } 39 40 char* ptr(){ return chars.ptr;} 41 42 private void initialize(size_t length) 43 { 44 if(length == 0) 45 length = 128; 46 this.countPtr = cast(int*)malloc(int.sizeof); 47 this.chars = (cast(char*)malloc(length))[0..0]; 48 this._capacity = length; 49 this.chars.ptr[0.._capacity] = '\0'; 50 *countPtr = 1; 51 } 52 53 static auto opCall(string str) 54 { 55 String s; 56 s.initialize(str.length); 57 s.chars = s.chars.ptr[0..str.length]; 58 s.chars[] = str[]; 59 return s; 60 } 61 static auto opCall(const(char)* str){return opCall(str[0..strlen(str)]);} 62 static auto opCall(String str){return str;} 63 64 private enum isAppendable(T) = is(T == string) || is(T == immutable(char)*) || is(T == char); 65 66 version(AvoidStringFragmentation) 67 { 68 static auto opCall(Args...)(Args args) 69 { 70 import hip.util.conv:toStringRange; 71 rcStringBuffer.clear(); 72 static foreach(a; args) 73 { 74 static if(isAppendable!(typeof(a)) ) 75 rcStringBuffer~= a; 76 else static if(__traits(hasMember, a, "toString")) 77 rcStringBuffer~= a.toString; 78 else static if(is(typeof(a) == struct) || __traits(compiles, toStringRange(rcStringBuffer, a))) 79 { 80 toStringRange(rcStringBuffer, a); 81 } 82 // else static if(is(typeof(a) == String[])) 83 // { 84 // foreach(str; a) 85 // s~= str; 86 // } 87 else 88 static assert(false, "No conversion found for type "~typeof(a).stringof); 89 } 90 static foreach_reverse(a; args) 91 { 92 static if(is(typeof(a) == String)) 93 destroy(a); 94 } 95 return String(rcStringBuffer.toString()); 96 } 97 auto ref opOpAssign(string op, T)(T value) 98 if(op == "~") 99 { 100 rcStringBuffer.clear(); 101 char[] chs; 102 static if(is(T == String)) 103 chs = value.chars; 104 else static if (is(T == string) || is(T == char[])) 105 chs = cast(char[])value; 106 else static if(is(T == immutable(char)*)) 107 chs = value[0..strlen(value)]; 108 else static if(is(T == char)) 109 { 110 char[1] _chContainer; 111 _chContainer[0] = value; 112 chs = _chContainer; 113 } 114 else 115 { 116 toStringRange(rcStringBuffer, value); 117 chs = cast(char[])rcStringBuffer.toString(); 118 } 119 if(!updateBorrowed(chs.length) && chs.length + this.length >= this._capacity) //New size is greater than capacity 120 resize(cast(uint)((chs.length + this.length)*1.5)); 121 memcpy(chars.ptr+length, chs.ptr, chs.length); 122 chars = chars.ptr[0..chars.length+chs.length]; 123 return this; 124 } 125 } 126 else 127 { 128 static auto opCall(Args...)(Args args) 129 { 130 import hip.util.conv:toStringRange; 131 String s; 132 s.initialize(128); 133 static foreach(a; args) 134 { 135 static if(isAppendable!(typeof(a)) ) 136 s~= a; 137 else static if(__traits(hasMember, a, "toString")) 138 s~= a.toString; 139 else static if(is(typeof(a) == struct) || __traits(compiles, toStringRange(s, a))) 140 { 141 toStringRange(s, a); 142 } 143 // else static if(is(typeof(a) == String[])) 144 // { 145 // foreach(str; a) 146 // s~= str; 147 // } 148 else static assert(false, "No conversion found for type "~typeof(a).stringof); 149 } 150 return s; 151 } 152 auto ref opOpAssign(string op, T)(T value) 153 if(op == "~") 154 { 155 String temp; 156 char[] chs; 157 static if(is(T == String)) 158 chs = value.chars; 159 else static if (is(T == string) || is(T == char[])) 160 chs = cast(char[])value; 161 else static if(is(T == immutable(char)*)) 162 chs = value[0..strlen(value)]; 163 else static if(is(T == char)) 164 { 165 char[1] _chContainer; 166 _chContainer[0] = value; 167 chs = _chContainer; 168 } 169 else 170 { 171 temp = String(value); 172 chs = temp.chars; 173 } 174 if(!updateBorrowed(chs.length) && chs.length + this.length >= this._capacity) //New size is greater than capacity 175 resize(cast(uint)((chs.length + this.length)*1.5)); 176 memcpy(chars.ptr+length, chs.ptr, chs.length); 177 chars = chars.ptr[0..chars.length+chs.length]; 178 return this; 179 } 180 } 181 182 183 alias _opApplyFn = int delegate(char c) @nogc; 184 int opApply(scope _opApplyFn dg) 185 { 186 int result = 0; 187 for(int i = 0; i < length && result; i++) 188 result = dg(chars[i]); 189 return result; 190 } 191 192 /** 193 * If it was borrowed, allocate new memory. 194 */ 195 bool updateBorrowed(size_t length) 196 { 197 if(countPtr == null) //Not initialized 198 { 199 initialize(length); 200 return true; 201 } 202 else if(*countPtr != 1) //If it is borrowed 203 { 204 //Remove that old reference and initialize itself (something like when slices shares a common array) 205 char[] oldChars = chars; 206 *countPtr = *countPtr - 1; 207 initialize(length+this.length); 208 chars = chars.ptr[0..oldChars.length]; 209 chars[0..oldChars.length] = oldChars[0..$]; 210 return true; 211 } 212 return false; 213 } 214 215 216 auto ref opAssign(string value) 217 { 218 if(countPtr is null) 219 chars = cast(char[])value; //Don't allocate memory for the string literal. 220 else 221 { 222 bool resized = updateBorrowed(value.length); 223 if(!resized) 224 { 225 if(chars == null) 226 initialize(value.length); 227 else if(value.length > _capacity) 228 resize(value.length); 229 } 230 chars.ptr[0..value.length] = value[]; 231 } 232 return this; 233 } 234 235 auto ref opAssign(immutable(char)* value) 236 { 237 opAssign(value[0..strlen(value)]); 238 return this; 239 } 240 241 242 bool opCast(T: bool)() const nothrow { return chars.ptr != null; } 243 244 string opCast(T: string)() const 245 { 246 return cast(string)chars[0..length]; 247 } 248 249 String opSlice(size_t start, size_t end) nothrow 250 { 251 assert(countPtr != null, "Can't slice a null pointer."); 252 String ret; 253 ret.countPtr = countPtr; 254 ret.chars = chars.ptr[start..end]; 255 ret._capacity = _capacity; 256 *countPtr = *countPtr+1; 257 return ret; 258 } 259 260 /** 261 * BEWARE: If you're creating a String from a String.toString call, that String will 262 * cause memory fragmentation, which will make WebAssembly not reuse that memory block. 263 ```d 264 String s = String(String("Hello").toString); 265 ``` 266 * Since s won't recognize that "Hello" as a 267 * 268 * Returns: 269 */ 270 string toString() const pure nothrow @trusted 271 { 272 return cast(string)chars; 273 } 274 275 pragma(inline, true) private void resize(size_t newSize) 276 { 277 chars = (cast(char*)realloc(chars.ptr, newSize))[0..chars.length]; 278 _capacity = newSize; 279 } 280 ///Make this struct OutputRange compatible 281 void put(char c) 282 { 283 if(this.length + 1 >= this._capacity) 284 resize(cast(uint)((this.length+1)*1.5)); 285 chars.ptr[length] = c; 286 chars = chars.ptr[0..length+1]; 287 } 288 bool opEquals(R)(const R other) const 289 { 290 static if(is(R == typeof(null))) 291 return chars == null; 292 else static if(is(R == string)) 293 return toString == other; 294 else static if(is(R == String)) 295 return toString == other.toString; 296 else static assert(false, "Invalid comparison between String and "~R.stringof); 297 } 298 299 /** 300 * This function serves to allocate before put. This will make less allocations occur while iterating 301 * this struct as an OutputRange. 302 */ 303 void preAllocate(uint howMuch) 304 { 305 if(length + howMuch > _capacity) 306 resize(_capacity + howMuch); 307 } 308 void preAllocate(ulong howMuch){preAllocate(cast(uint)howMuch);} 309 310 ref auto opIndex(size_t index) const 311 { 312 assert(index < length, "Index out of bounds"); 313 return chars[index]; 314 } 315 316 ~this() nothrow @trusted pure 317 { 318 import core.memory; 319 if(countPtr != null) 320 { 321 *countPtr = *countPtr - 1; 322 assert(*countPtr >= 0); 323 if(*countPtr == 0) 324 { 325 pureFree(chars.ptr); 326 pureFree(countPtr); 327 } 328 countPtr = null; 329 chars = null; 330 } 331 } 332 333 } 334 335 /** 336 * Creates a stack string. This does not use any heap allocation. 337 * Prefer using that one inside game loop 338 */ 339 struct StringBuffer(size_t capacity) 340 { 341 @nogc: 342 private char[capacity] chars; 343 private size_t _length; 344 345 pragma(inline, true) 346 size_t length() @safe pure const nothrow { return _length; } 347 348 /** 349 * Returns: An empty StringBuffer which avoids initialization on its buffer 350 */ 351 static StringBuffer!(capacity) get() 352 { 353 StringBuffer!(capacity) ret = void; 354 ret._length = 0; 355 return ret; 356 } 357 358 static auto opCall(Args...)(Args args) 359 { 360 auto ret = StringBuffer!(capacity).get(); 361 static foreach(a; args) 362 ret~= a; 363 return ret; 364 } 365 366 void preAllocate(size_t howMuch) 367 { 368 assert(length + howMuch < capacity, "Can't preallocate more to string buffer."); 369 } 370 void put(char c){chars[_length++] = c;} 371 void put(const(char)[] s){chars[_length.._length+s.length] = s[]; _length+= s.length;} 372 void put(immutable(char)* s){put(s[0..strlen(s)]);} 373 void put(String s){put(s.toString());} 374 375 StringBuffer opSlice(size_t start, size_t end) 376 { 377 assert(end >= start, "Slice end must be greater or equal than start."); 378 StringBuffer ret = void; 379 ret.chars[0..end-start] = chars[start..end]; 380 ret._length = end - start; 381 return ret; 382 } 383 384 pragma(inline, true) 385 ref char opIndex(size_t index) 386 { 387 return chars[index]; 388 } 389 390 void opOpAssign(string op, T)(T value) 391 if(op == "~") 392 { 393 static if(is(T == char) || is(T : const(char)[]) || is(T == immutable(char*)) || is(T == String)) 394 put(value); 395 else 396 toStringRange(this, value); 397 } 398 void clear(){_length = 0;} 399 bool opCast(T : bool)() const{return _length != 0;} 400 bool opEquals(const string other) const{return chars[0.._length] == other;} 401 string toString() const @trusted {return cast(string)chars[0.._length];} 402 } 403 404 alias BigString = StringBuffer!(8192); 405 alias PathString = StringBuffer!(2048); 406 alias SmallString = StringBuffer!(256); 407 408 409 version(AvoidStringFragmentation) 410 { 411 ///On WebAssembly, it is required to have a refcounted string buffer for avoiding memory fragmentation, which causes refcounted strings to leak memory when appended 412 private StringBuffer!(8192) rcStringBuffer; 413 } 414 415 struct StringBuilder 416 { 417 private char[] builtString; 418 private uint builtLength; 419 string[] strings; 420 private uint stringsPtr = 0; 421 422 void append(T)(T value) 423 { 424 if(stringsPtr == strings.length) 425 { 426 if(strings.length == 0x10000) //65K (This will guarantee a reasonable amount of allocations) 427 toString(); 428 else 429 { 430 //128 is a reasonable start, this way, no really small operation should matter on performance 431 strings.length = strings.length == 0 ? 128 : strings.length * 2; 432 } 433 } 434 strings[stringsPtr++] = value; 435 } 436 string toString() 437 { 438 import core.stdc.string:memcpy; 439 if(stringsPtr == 0) return cast(string)builtString[0..builtLength]; 440 uint count = builtLength; 441 uint i = builtLength; 442 foreach(s;strings[0..stringsPtr]) 443 count+= s.length; 444 builtString.length = count; 445 446 foreach(s; strings[0..stringsPtr]) 447 { 448 memcpy(builtString.ptr+i, s.ptr, s.length); 449 i+= s.length; 450 } 451 builtLength = count; 452 stringsPtr = 0; 453 return cast(string)builtString[0..builtLength]; 454 } 455 auto ref opAssign(T)(T value) if(is(T == string)) 456 { 457 builtString.length = value.length; 458 foreach(i, c; s) 459 builtString[i] = c; 460 stringsPtr = 0; 461 builtLength = cast(typeof(builtLength))value.length; 462 463 return this; 464 } 465 auto ref opOpAssign(string op, T)(T value) if(op == "~") 466 { 467 import hip.util.reflection:isArray; 468 static if(isArray!T && !is(T == string)) 469 foreach(v; value) append(v); 470 else 471 append(value); 472 return this; 473 } 474 ref auto opIndex(size_t index){return toString()[index];} 475 uint length(){return builtLength;} 476 ~this(){strings.length = 0;} 477 478 ///Interface for OutputRange 479 alias put = append; 480 } 481 482 483 pure dstring toUTF32(string encoded) 484 { 485 dstring decoded; 486 version(UseDRuntimeDecoder) 487 { 488 foreach(dchar ch; encoded) decoded~= ch; 489 } 490 else 491 { 492 static import std.utf; 493 decoded = std.utf.toUTF32(encoded); 494 } 495 return decoded; 496 } 497 498 pure string replaceAll(string str, char what, string replaceWith = "") @trusted nothrow 499 { 500 if(replaceWith.length == 1) 501 { 502 import hip.util.array; 503 char[] ret = uninitializedArray!(char[])(str.length); 504 foreach(i, ch; str) 505 { 506 if(ch == what) 507 ret[i] = replaceWith[0]; 508 else 509 ret[i] = ch; 510 } 511 return cast(string)ret; 512 } 513 else 514 { 515 string ret; 516 for(int i = 0; i < str.length; i++) 517 { 518 if(str[i] != what) ret~= str[i]; 519 else if(replaceWith != "") ret~=replaceWith; 520 } 521 return ret; 522 } 523 } 524 525 pure string replaceAll(string str, string what, string replaceWith = "") 526 { 527 char[] ret; 528 int last; 529 int i; 530 do 531 { 532 i = indexOf(str, what, i); 533 if(i != -1) 534 { 535 int copyLength = i - last; 536 int currLength = cast(int)ret.length; 537 ret.length+= copyLength+replaceWith.length; 538 //Copy old content 539 ret[currLength..currLength+copyLength] = str[last..i]; 540 //Copy replace 541 ret[currLength+copyLength..$] = replaceWith[]; 542 //Skip what 543 i+= what.length; 544 last = i; 545 } 546 } while(i != -1); 547 548 int copyLength = cast(int)(str.length - last); 549 int currLength = cast(int)ret.length; 550 ret.length+= copyLength; 551 ret[currLength..$] = str[last..$]; 552 553 return cast(string)ret; 554 } 555 556 pure int indexOf(String str, const char[] toFind, int startIndex = 0) nothrow @nogc @safe 557 { 558 return indexOf(str.toString, toFind, startIndex); 559 } 560 561 pure int indexOf(const char[] str, const char[] toFind, int startIndex = 0) nothrow @nogc @safe 562 { 563 if(!toFind.length) 564 return -1; 565 int left = 0; 566 567 for(int i = startIndex; i < str.length; i++) 568 { 569 if(str[i] == toFind[left]) 570 { 571 left++; 572 if(left == toFind.length) 573 return (i+1) - left; //Remember that left is already out of bounds 574 } 575 else if(left > 0) 576 left--; 577 } 578 return -1; 579 } 580 581 pure bool startsWith(inout string str, inout string withWhat) nothrow @nogc @safe 582 { 583 if(withWhat.length > str.length) 584 return false; 585 return str[0..withWhat.length] == withWhat; 586 } 587 588 /** 589 * Same thing as startsWith, but returns the part after the afterWhat 590 */ 591 pure string after(string str, const string afterWhat) nothrow @nogc @safe 592 { 593 if(afterWhat.length > str.length || str[0..afterWhat.length] != afterWhat) return null; 594 return str[afterWhat.length..$]; 595 } 596 597 pure inout(string) findAfter(inout string str, inout string afterWhat, int startIndex = 0) nothrow @nogc @safe 598 { 599 int afterWhatIndex = str.indexOf(afterWhat, startIndex); 600 if(afterWhatIndex == -1) 601 return null; 602 return str[afterWhatIndex+afterWhat.length..$]; 603 } 604 605 /** 606 * Returns the content that is between `left` and `right`: 607 ```d 608 string test = `string containing a "thing"`; 609 writeln(test.between(`"`, `"`)); //thing 610 ``` 611 */ 612 pure inout(string) between(inout string str, inout string left, inout string right, int start = 0) nothrow @nogc @safe 613 { 614 int leftIndex = str.indexOf(left, start); 615 if(leftIndex == -1) return null; 616 int rightIndex = str.indexOf(right, leftIndex+1); 617 if(rightIndex == -1) return null; 618 619 return str[leftIndex+1..rightIndex]; 620 } 621 622 pure int indexOf(const string str, char ch, int startIndex = 0) nothrow @nogc @trusted 623 { 624 for(; startIndex < str.length; startIndex++) 625 if(str[startIndex] == ch) 626 return startIndex; 627 return -1; 628 } 629 630 631 string repeat(string str, size_t repeatQuant) 632 { 633 string ret; 634 for(int i = 0; i < repeatQuant; i++) 635 ret~= str; 636 return ret; 637 } 638 639 pure int count(const string str, const string countWhat) nothrow @nogc @safe 640 { 641 int ret = 0; 642 int index = 0; 643 644 //Navigates using indexOf 645 while((index = str.indexOf(countWhat, index)) != -1) 646 { 647 index+= countWhat.length; 648 ret++; 649 } 650 return ret; 651 } 652 653 alias countUntil = indexOf; 654 655 int lastIndexOf(const string str, const string toFind, int startIndex = -1) pure nothrow @nogc @safe 656 { 657 if(startIndex == -1) startIndex = cast(int)(str.length)-1; 658 659 int maxToFind = cast(int)toFind.length - 1; 660 int right = maxToFind; 661 if(right < 0) return -1; //Empty string case 662 663 664 for(int i = startIndex; i >= 0; i--) 665 { 666 if(str[i] == toFind[right]) 667 { 668 right--; 669 if(right == -1) 670 return i; 671 } 672 else if(right < maxToFind) 673 right++; 674 } 675 return -1; 676 } 677 int lastIndexOf(string str, char ch, int startIndex = -1) pure nothrow @nogc @trusted 678 { 679 return lastIndexOf(str, cast(string)(&ch)[0..1], startIndex); 680 } 681 682 T toDefault(T)(string s, T defaultValue = T.init) 683 { 684 if(s == "") 685 return defaultValue; 686 T v = defaultValue; 687 try{v = to!(T)(s);} 688 catch(Exception e){} 689 return v; 690 } 691 692 string fromStringz(const char* cstr) pure nothrow @nogc 693 { 694 import core.stdc.string:strlen; 695 size_t len = strlen(cstr); 696 return (len) ? cast(string)cstr[0..len] : null; 697 } 698 699 const(char)* toStringz(string str) pure nothrow 700 { 701 return (str~"\0").ptr; 702 } 703 pragma(inline, true) char toLowerCase(char c) pure nothrow @safe @nogc 704 { 705 return (c < 'A' || c > 'Z') ? c : cast(char)(c + ('a' - 'A')); 706 } 707 708 string toLowerCase(string str) 709 { 710 char[] ret = new char[](str.length); 711 for(uint i = 0; i < str.length; i++) 712 ret[i] = str[i].toLowerCase; 713 return cast(string)ret; 714 } 715 716 pragma(inline, true) char toUpper(char c) pure nothrow @nogc @safe 717 { 718 if(c < 'a' || c > 'z') 719 return c; 720 return cast(char)(c - ('a' - 'A')); 721 } 722 723 string toUpper(string str) pure nothrow @safe 724 { 725 char[] ret = new char[](str.length); 726 for(uint i = 0; i < str.length; i++) 727 ret[i] = str[i].toUpper; 728 return ret; 729 } 730 731 string[] split(string str, char separator) pure nothrow 732 { 733 return split(str, cast(string)(&separator)[0..1]); 734 } 735 736 string[] split(string str, string separator) pure nothrow @safe 737 { 738 string[] ret; 739 int last = 0; 740 int index = 0; 741 do 742 { 743 index = str.indexOf(separator, index); 744 if(index != -1) 745 { 746 ret~= str[last..index]; 747 last = index+= separator.length; 748 } 749 } 750 while(index != -1); 751 if(last != index) 752 ret~= str[last..$]; 753 return ret; 754 } 755 756 auto splitRange(TString, TStrSep)(TString str, TStrSep separator) pure nothrow @safe @nogc 757 { 758 struct SplitRange 759 { 760 TString strToSplit; 761 TStrSep sep; 762 TString frontStr; 763 int lastFound, index; 764 765 bool empty(){return frontStr == null && index == -1 && lastFound == -1;} 766 TString front() 767 { 768 if(frontStr == "") popFront(); 769 return frontStr; 770 } 771 void popFront() 772 { 773 if(index == -1 && lastFound == -1) 774 { 775 frontStr = null; 776 return; 777 } 778 index = indexOf(cast(TString)strToSplit, cast(TStrSep)sep, index); 779 //When finding, take the string[lastFound..index] 780 if(index != -1) 781 { 782 frontStr = strToSplit[lastFound..index]; 783 lastFound = index+= sep.length; 784 } 785 //If index not found and there was a last, take the string[lastFound..$] 786 else if(lastFound != 0) 787 { 788 frontStr = strToSplit[lastFound..$]; 789 lastFound = -1; 790 } 791 //Just say there is no string 792 else 793 lastFound = -1; 794 } 795 } 796 797 return SplitRange(str, separator); 798 } 799 800 801 bool isNumber(const string str) nothrow @nogc 802 { 803 if(!str) 804 return false; 805 bool isFirst = true; 806 bool hasDecimalSeparator = false; 807 for(int i = 0; i < str.length; i++) 808 { 809 char c = str[i]; 810 //Check for negative 811 if(isFirst) 812 { 813 isFirst = false; 814 if(c == '-') 815 continue; 816 } 817 //Can only check for '.' once. 818 if(!hasDecimalSeparator && c == '.') 819 hasDecimalSeparator = true; 820 else if(c < '0' || c > '9') 821 return false; 822 823 } 824 return true; 825 } 826 827 pragma(inline, true) 828 string toString(string s) nothrow @nogc pure @safe { return s; } 829 830 /** 831 * Returns the entire string if input is not a number separated by a '.' 832 * 833 * 834 * Params: 835 * input = A input string in "523.987" 836 * decimalPlaces = How many decimal places it must contain. 837 * Returns: A slice which removes places after the decimal case if there exists more than it should 838 */ 839 string limitDecimalPlaces(string input, ubyte decimalPlaces) @nogc nothrow 840 { 841 string str = input.toString; 842 if(!isNumber(str)) 843 return input; 844 ptrdiff_t decIndex = indexOf(str, "."); 845 if(decIndex == -1) 846 return input; 847 848 //+1 since there is also the dot 849 size_t end = decIndex+1+decimalPlaces; 850 if(end > input.length) 851 end = input.length; 852 return input[0..end]; 853 854 } 855 856 /** 857 This function will get the number at the end of the string. Used when you have numbered items such as frames: 858 walk_01, walk_02, etc 859 ```d 860 "test123".getNumericEnding == "123" 861 "123abc".getNumericEnding == "" 862 "123".getNumericEnding == "123" 863 ``` 864 */ 865 string getNumericEnding(string s) 866 { 867 if(!s) 868 return ""; 869 ptrdiff_t i = cast(ptrdiff_t)s.length - 1; 870 while(i >= 0) 871 { 872 if(!isNumeric(s[i])) 873 return s[i+1..$]; 874 i--; 875 } 876 return s; 877 } 878 879 880 pragma(inline, true) bool isUpperCase(TChar)(TChar c) @nogc nothrow pure @safe 881 { 882 return c >= 'A' && c <= 'Z'; 883 } 884 pragma(inline, true) bool isLowercase(TChar)(TChar c) @nogc nothrow pure @safe 885 { 886 return c >= 'a' && c <= 'z'; 887 } 888 889 pragma(inline, true) bool isAlpha(TChar)(TChar c) @nogc nothrow pure @safe 890 { 891 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); 892 } 893 894 pragma(inline, true) bool isEndOfLine(TChar)(TChar c) @nogc nothrow pure @safe 895 { 896 return c == '\n' || c == '\r'; 897 } 898 899 pragma(inline, true) bool isNumeric(TChar)(TChar c) @nogc nothrow pure @safe 900 { 901 return (c >= '0' && c <= '9') || (c == '-'); 902 } 903 pragma(inline, true) bool isWhitespace(TChar)(TChar c) @nogc nothrow pure @safe 904 { 905 return (c == ' ' || c == '\t' || c.isEndOfLine); 906 } 907 908 string trim(string str) pure nothrow @safe @nogc 909 { 910 size_t start = 0; 911 size_t end = str.length; 912 while(start < end && str[start].isWhitespace) 913 start++; 914 while(end > start && str[end-1].isWhitespace) 915 end--; 916 return str[start..end]; 917 } 918 919 string join(string[] args, string separator = "") 920 { 921 string ret = args.length > 0 ? args[0] : null; 922 for(int i = 1; i < args.length; i++) 923 ret~= separator~args[i]; 924 return ret; 925 } 926 927 unittest 928 { 929 assert(join(["hello", "world"], ", ") == "hello, world"); 930 assert(split("hello world", " ").length == 2); 931 assert(toDefault!int("hello") == 0); 932 assert(lastIndexOf("hello, hello", "hello") == 7); 933 assert(indexOf("hello, hello", "hello") == 0); 934 assert(replaceAll("\nTest\n", '\n') == "Test"); 935 936 assert(trim(" \n \thello there \n \t") == "hello there"); 937 assert(between(`string containing a "thing"`, `"`, `"`) == "thing"); 938 939 assert("test123".getNumericEnding == "123"); 940 assert("123abc".getNumericEnding == ""); 941 assert("123".getNumericEnding == "123"); 942 }